package codegen.builders;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;

import org.apache.log4j.Logger;

import uk.co.q3c.deplan.client.domain.task.BaseTask;
import uk.co.q3c.deplan.client.util.Q3StringUtil;

/**
 * Base class to generate classes and interfaces, {@link CodegenClass} and {@link CodegenInterface}
 * 
 * @author David Sowerby 19 May 2010
 * 
 */
public abstract class CodegenType {
	protected Set<String> primitiveNames;
	transient protected final Logger logger = Logger.getLogger(getClass().getName());
	private final Set<String> imports;
	private String extend;
	private final Set<String> implement;
	protected final Set<CodegenField> fields;
	protected final Map<String, CodegenMethod> methods;
	private final Set<CodegenConstructor> constructors;
	private String simpleName;
	private String packageName;
	int indent;
	public final String term = ";\n";
	private final Visibility visibility = Visibility.PUBLIC;

	public CodegenType(String simnpleName) {
		super();
		imports = new TreeSet<String>();
		implement = new TreeSet<String>();
		fields = new TreeSet<CodegenField>();
		methods = new TreeMap<String, CodegenMethod>();
		constructors = new TreeSet<CodegenConstructor>();
		this.simpleName = simnpleName;
		primitiveNames = new TreeSet<String>();
		primitiveNames.add(boolean.class.getName());
		primitiveNames.add(int.class.getName());
		primitiveNames.add(float.class.getName());
		primitiveNames.add(double.class.getName());
		primitiveNames.add(byte.class.getName());
		primitiveNames.add(short.class.getName());
		primitiveNames.add(Object.class.getName());
		primitiveNames.add(long.class.getName());
		primitiveNames.add(char.class.getName());
		primitiveNames.add(String.class.getName());
		primitiveNames.add("void");
	}

	public String getSimpleName() {
		return simpleName;
	}

	public CodegenType simpleName(String name) {
		this.simpleName = name;
		return this;
	}

	public void outputToFile(String projectRoot) throws IOException {

		String tp = packageName.replace('.', '/');
		File outputFile = new File(projectRoot + "/" + tp + "/" + simpleName + ".java");
		BufferedWriter fw = new BufferedWriter(new FileWriter(outputFile));
		// BufferedWriter fw = new BufferedWriter(new FileWriter(outputFile));
		writePackageHeader(fw);
		writeImports(fw);
		writeClassOpen(fw);
		writeFields(fw);
		writeConstructors(fw);
		writeMethods(fw);
		indent--;
		writeClassClose(fw);
		fw.close();

	}

	private void writeClassClose(BufferedWriter fw) throws IOException {
		writeIndent(fw);
		fw.write("}");
	}

	protected void writeMethods(BufferedWriter fw) throws IOException {
		for (CodegenMethod method : methods.values()) {
			newline(fw);
			writeIndent(fw);
			method.write(fw);
			newline(fw);
		}
	}

	private void writeConstructors(BufferedWriter fw) throws IOException {
		for (CodegenConstructor constructor : constructors) {
			writeIndent(fw);
			constructor.write(fw);
		}
	}

	private void writeFields(BufferedWriter fw) throws IOException {
		for (CodegenField field : fields) {
			writeIndent(fw);
			field.write(fw);
			fw.write(term);
		}
		newline(fw);
	}

	/**
	 * @see CodegenClass#writeClassOpen(BufferedWriter)
	 * @see CodegenInterface#writeClassOpen(BufferedWriter)
	 * @param fw
	 * @throws IOException
	 */
	protected abstract void writeClassOpen(BufferedWriter fw) throws IOException;

	protected void doWriteClassOpen(boolean isInterface, BufferedWriter fw) throws IOException {
		StringBuffer buf = new StringBuffer();
		String type = (isInterface) ? " interface " : " class ";
		buf.append(visibility.getText() + type + simpleName + " ");
		if (extend != null) {
			buf.append("extends " + Q3StringUtil.simpleNameFromClassName(extend));
			buf.append(" ");
		}
		boolean first = true;
		for (String imp : implement) {
			String s = Q3StringUtil.simpleNameFromClassName(imp);
			if (!first) {
				buf.append(", " + s);
			} else {
				buf.append("implements " + s);
				first = false;
			}
		}
		buf.append(" {\n\n");
		fw.write(buf.toString());
		indent++;
	}

	private void writeImports(BufferedWriter fw) throws IOException {
		for (String clazz : imports) {
			writeIndent(fw);
			fw.write("import " + clazz + term);
		}
		newline(fw);
	}

	protected void newline(BufferedWriter fw) throws IOException {
		fw.write("\n");
	}

	private void writePackageHeader(BufferedWriter fw) throws IOException {
		String fileHeader = "package " + packageName + ";\n\n";
		fw.write(fileHeader);
	}

	public void writeIndent(BufferedWriter fw) throws IOException {
		for (int c = 0; c < indent; c++) {
			fw.write("\t");
		}
	}

	public CodegenField addField(String name, String fullClassName) {
		try {
			ClassLoader cl = BaseTask.class.getClassLoader();
			Class<?> clazz = Class.forName(fullClassName, false, cl);
			return addField(name, clazz);
		} catch (ClassNotFoundException e) {
			logger.error("Could not create class for name " + fullClassName);
			e.printStackTrace();
			return null;
		}
	}

	public CodegenField addField(String name, Class<?> clazz) {
		if (clazz == null) {
			logger.error("clazz cannot be null, field name is " + name);
		}
		addImport(clazz.getName());
		CodegenField f = new CodegenField(this, name, clazz);
		fields.add(f);
		return f;
	}

	public CodegenMethod addMethod(String name) {
		CodegenMethod m = new CodegenMethod(this, name);
		methods.put(m.getName(), m);
		return m;
	}

	public CodegenConstructor addConstructor() {
		CodegenConstructor m = new CodegenConstructor(this, getClass());
		constructors.add(m);
		return m;
	}

	public String getPackageName() {
		return packageName;
	}

	public CodegenType packageName(String packageName) {
		this.packageName = packageName;
		return this;
	}

	public CodegenType extend(String fullClassname) {
		extend = fullClassname;
		return this;
	}

	public CodegenType extend(Class<?> clazz) {
		addImport(clazz);
		extend = clazz.getName();
		return this;
	}

	public CodegenType implement(String fullInterfaceName) {
		implement.add(fullInterfaceName);
		return this;
	}

	public CodegenType implement(Class<?> clazz) {
		implement.add(clazz.getName());
		return this;
	}

	public CodegenType addImport(Class<?> clazz) {
		if (!clazz.isPrimitive()) {
			imports.add(clazz.getName());
		}
		return this;
	}

	public CodegenType addImport(String fullClassname) {
		logger.debug("add import " + fullClassname);
		if (!primitiveNames.contains(fullClassname)) {
			imports.add(fullClassname);
		} else {
			logger.info("import for " + fullClassname + " not added");
		}

		return this;
	}

	public String fullName() {
		return packageName + "." + simpleName;
	}

	public void removeMethod(String methodName) {
		methods.remove(methodName);
	}

	/**
	 * Returns the default constructor (identified as the one without parameters), or creates one if necessary
	 * 
	 * @return
	 */
	public CodegenConstructor getDefaultConstructor() {

		for (CodegenConstructor constructor : constructors) {
			if (constructor.isDefault()) {
				return constructor;
			}
		}
		return (CodegenConstructor) addConstructor().addBodyLine("super()");
	}
}
